﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using JigLibX.Geometry;

namespace Library
{
    public class Terrain : Component, I3DComponent
    {
        public float[,] heightData;
        HeightMapInfo heightMapInfo;
        HeightMapObject heightMapObject;
        Texture2D texture;

        VertexDeclaration vertexDeclaration;
        VertexBuffer vertexBuffer;
        IndexBuffer indexBuffer;

        BasicEffect basicEffect;
        Vector3 position = Vector3.Zero;
        Matrix rotation = Matrix.Identity;
        Vector3 scale = new Vector3(1, 1, -1);
        BoundingBox boundingBox = new BoundingBox(new Vector3(-1), new Vector3(1));

        public Vector3 Position {get{return position;} set{position = value;}}
        public Vector3 EulRotation{get{return MathUtils.MatrixToVector3(rotation); }
            set{this.rotation = MathUtils.Vector3ToMatrix(value);}}

        public Matrix Rotation { get { return rotation; } set { rotation = value; } }

        public Vector3 Scale { get { return scale; } set { scale = value; } }
        public BoundingBox BoundingBox {get{return boundingBox;}}

        public Terrain(Texture2D Heightmap, Texture2D Texture)
            : base()
        {
            Setup(Heightmap, Texture);
        }

        public Terrain(Texture2D Heightmap, Texture2D Texture, Screen Parent)
            : base(Parent)
        {
            Setup(Heightmap, Texture);
        }

        void Setup(Texture2D Heightmap, Texture2D Texture)
        {
            heightData = CreateTerrain(Heightmap, Texture);

            vertexDeclaration = new VertexDeclaration(Engine.GraphicsDevice, VertexPositionNormalTexture.VertexElements);
            VertexPositionNormalTexture[] terrainVertices = CreateVertices();
            int[] terrainIndices = CreateIndices();
            terrainVertices = GenerateNormalsForTriangleStrip(terrainVertices, terrainIndices);
            CreateBuffers(terrainVertices, terrainIndices);

            basicEffect = new BasicEffect(Engine.GraphicsDevice, null);
            SetupEffect();
        }

        private float[,] CreateTerrain(Texture2D heightMap, Texture2D texture)
        {
            float minHeight = 0;
            float maxHeight = 255;
            int width = heightMap.Width;
            int height = heightMap.Height;

            boundingBox = new BoundingBox(
                new Vector3(-width / 2, maxHeight - minHeight, -height / 2),
                new Vector3(width / 2, maxHeight - minHeight, height / 2));

            this.texture = texture;
            Color[] heightMapColor = new Color[width * height];
            heightMap.GetData<Color>(heightMapColor);

            float[,] heightData = new float[width, height];
            for (int x = 0; x < width; x++)
                for (int y = 0; y < height; y++)
                {
                    heightData[x, y] = heightMapColor[x + y * width].R;
                    if(heightData[x,y] < minHeight)
                        minHeight = heightData[x,y];
                    if (heightData[x, y] > maxHeight)
                        maxHeight = heightData[x, y];
                }

            for (int x = 0; x < width; x++)
                for (int y = 0; y < height; y++)
                    heightData[x, y] = (heightData[x, y] - minHeight)/(maxHeight-minHeight)*30.0f;


            heightMapInfo = new HeightMapInfo(heightData, 1);
            if(heightMapObject != null)
            {
                heightMapObject.DisableComponent();
                heightMapObject = null;
            }

            heightMapObject = new HeightMapObject(heightMapInfo, 
                new Vector2(heightMapInfo.Width / 2, -heightMapInfo.Height / 2 + heightMapInfo.Height));
            return heightData;
        }

        private VertexPositionNormalTexture[] CreateVertices()
        {
            int width = heightData.GetLength(0);
            int height = heightData.GetLength(1);

            VertexPositionNormalTexture[] terrainVertices = new VertexPositionNormalTexture[width * height];

            int i = 0;
            for(int z = 0; z<height; z++)
                for (int x = 0; x < width; x++)
                {
                    Vector3 position = new Vector3(x, heightData[x, z], -z);
                    Vector3 normal = new Vector3(0, 0, 1);
                    Vector2 texCoord = new Vector2((float)x / 30.0f, (float)z / 30.0f);

                    terrainVertices[i++] = new VertexPositionNormalTexture(position, normal, texCoord);
                }
            return terrainVertices;
        }

        private int[] CreateIndices()
        {
            int width = heightData.GetLength(0);
            int height = heightData.GetLength(1);
            int[] terrainIndices = new int[(width) * 2 * (height - 1)];

            int i = 0;
            int z = 0;
            while (z < height - 1)
            {
                for (int x = 0; x < width; x++)
                {
                    terrainIndices[i++] = x + z * width;
                    terrainIndices[i++] = x + (z + 1) * width;
                }
                z++;
                if (z < height - 1)
                {
                    for (int x = width -1; x >= 0; x--)
                    {
                        terrainIndices[i++] = x + (z + 1) * width;
                        terrainIndices[i++] = x + z * width;
                    }
                }
                z++;
            }
            return terrainIndices;
        }

        private VertexPositionNormalTexture[] GenerateNormalsForTriangleStrip(
            VertexPositionNormalTexture[] vertices, int[] indices)
        {
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal = new Vector3(0, 0, 0);

            bool squish = false;
            for (int i = 2; i < indices.Length; i++)
            {
                Vector3 firstVec = vertices[indices[i - 1]].Position - vertices[indices[i]].Position;
                Vector3 secVec = vertices[indices[i - 2]].Position - vertices[indices[i]].Position;
                Vector3 normal = Vector3.Cross(firstVec, secVec);
                normal.Normalize();

                if (squish)
                    normal *= -1;

                if (!float.IsNaN(normal.X))
                {
                    vertices[indices[i]].Normal += normal;
                    vertices[indices[i-1]].Normal += normal;
                    vertices[indices[i-2]].Normal += normal;
                }
                squish = !squish;
            }

            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal.Normalize();

            return vertices;
        }

        private void CreateBuffers(VertexPositionNormalTexture[] vertices,
            int[] indices)
        {
            vertexBuffer = new VertexBuffer(Engine.GraphicsDevice,
                VertexPositionNormalTexture.SizeInBytes * vertices.Length,
                BufferUsage.WriteOnly);
            vertexBuffer.SetData(vertices);

            indexBuffer = new IndexBuffer(Engine.GraphicsDevice,
                typeof(int), indices.Length, BufferUsage.WriteOnly);
            indexBuffer.SetData(indices);
        }

        private void SetupEffect()
        {
            basicEffect.Texture = texture;
            basicEffect.TextureEnabled = true;
            basicEffect.EnableDefaultLighting();
            basicEffect.DirectionalLight0.Direction = new Vector3(1, -1, 1);
            basicEffect.DirectionalLight0.Enabled = true;
            basicEffect.AmbientLightColor = new Vector3(0.3f, 0.3f, 0.3f);
            basicEffect.DirectionalLight1.Enabled = false;
            basicEffect.DirectionalLight2.Enabled = false;
            basicEffect.SpecularColor = new Vector3(0, 0, 0);
            basicEffect.PreferPerPixelLighting = true;
        }

        public override void Draw()
        {
            Camera camera = Engine.Services.GetService<Camera>();
            if (camera == null)
                throw new Exception("The engine services does not contain a "
                + "camera service. The terrain requires a camera to draw.");

            basicEffect.World = MathUtils.CreateWorldMatrix(position,
                rotation, scale);
            basicEffect.View = camera.View;
            basicEffect.Projection = camera.Projection;
            
            // Get width and height
            int width = heightData.GetLength(0);
            int height = heightData.GetLength(1);
            
            Engine.GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;
            basicEffect.Begin();
            foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
            {
                pass.Begin();

                Engine.GraphicsDevice.Vertices[0].SetSource(vertexBuffer, 0,
                    VertexPositionNormalTexture.SizeInBytes);
                Engine.GraphicsDevice.Indices = indexBuffer;
                Engine.GraphicsDevice.VertexDeclaration = vertexDeclaration;
                Engine.GraphicsDevice.DrawIndexedPrimitives(
                    Microsoft.Xna.Framework.Graphics.PrimitiveType.TriangleStrip,
                    0, 0, width * height, 0, width * 2 * (height - 1) - 2);

                pass.End();
            }
            basicEffect.End();


            Engine.GraphicsDevice.RenderState.CullMode =
                CullMode.CullCounterClockwiseFace;
        }
    }
}

